/**
 * 
 */
package vn.smartware.co.java.net.ftp;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Vector;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import vn.smartware.co.java.net.commons.FileUtil;
import vn.smartware.co.java.net.commons.Utils;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

/**
 * JSFTPClient
 * @author thuctt
 */
public class JSFTPClient {
	
	private static Log log = LogFactory.getLog(JSFTPClient.class);
	private JSch jsch;
	private Session session = null;
	private Channel channel = null;
	
	private int retryCount = 0;
	private int RETRY_MAX = 5;
	private int retryInterval = 1;
	
	private String server;
	private String user;
	private String pass;
	private int port;
	private String hostkey;
	private String clientkey;
	
	/**
	 * Constructor
	 * @param host 
	 * @param user 
	 * @param port 
	 * @param pass 
	 */
	public JSFTPClient(String host, String user, String pass, int port){
		
		this.server = host;
		this.user = user;
		this.pass = pass;
		this.port = port;
	}
	
	/**
	 * Constructor
	 * @param server 
	 * @param user 
	 * @param port 
	 * @param hostkey 
	 * @param clientkey 
	 */
	public JSFTPClient(String server, String user, int port, String hostkey,
			String clientkey) {
		
		this.server = server;
		this.user = user;
		this.port = port;
		this.hostkey = hostkey;
		this.clientkey = clientkey;
	}
	
	/**
	 * connect to server
	 * @return boolean
	 */
	private boolean connect() {
		
		boolean res = true;
		try{
			jsch = new JSch();
            
			if (!Utils.isNullEmpty(this.pass)){
				//Create a session sending through our username and password
	            session = jsch.getSession(this.user, this.server, this.port);
	            log.info("Session created.");
	            session.setPassword(this.pass);
	            
			}else{
				jsch.setKnownHosts(new File(hostkey).getAbsolutePath());
				jsch.addIdentity(clientkey);
				//Create a session sending through our username and password
		        session = jsch.getSession(user, server, port);
		        log.info("Session created.");
			}
	        //
	        java.util.Properties config = new java.util.Properties();
	        config.put("StrictHostKeyChecking", "no");
	        session.setConfig(config);
	        
			session.connect();
	        log.info("Session connected.");
	        retryCount = 0;
			
		}catch (Exception e) {
			
			if (retryCount < RETRY_MAX) {
				retryCount++;
				log.error("Connect failed." + e);
				log.info("Connect Retry count=" + retryCount);
				try{
					Thread.sleep(retryInterval*1000);
				}catch (InterruptedException ee) {
					log.error("Thread sleep exception." + ee);
					return false;//FATAL error
				}
				res = connect();
				
			}else{
				log.error("Cannot connect to server.");
				return false;
			}
		}
		return res;
	}
	
	/**
	 * disconnect from server
	 * @return boolean
	 */
	private boolean disConnect() {

		boolean res = true;
		try{
			if (channel != null && channel.isConnected()){
				channel.disconnect();
			}
			if (session != null && session.isConnected()){
				session.disconnect();
			}
			jsch.removeAllIdentity();
			log.info("Disconnected.");
			retryCount = 0;
			
		}catch (JSchException e) {
			
			if (retryCount < RETRY_MAX) {
				retryCount++;
				log.error("Disconnect failed.");
				log.info("Disconnect Retry count=" + retryCount);
				try{
					Thread.sleep(retryInterval*1000);
				}catch (InterruptedException ee) {
					log.error("Thread sleep exception." + ee);
					return false;//FATAL error
				}
				res = connect();
				
			}else{
				log.error("Cannot disconnect from server.");
				return false;
			}
		}
	
		return res;
	}
	
	/**
	 * upload all files from client folder to SSH Server
	 * @param clientDir 
	 * @param remoteDir
	 * @return boolean
	 */
	public boolean upload(String clientDir, String remoteDir){
		
		clientDir = FileUtil.formatPath(clientDir);
		File path = new File(clientDir);
		if (!path.exists() || path.isFile()){
			log.error("Client Folder is invalid.");
			return false;
		}
		ArrayList<String> files = FileUtil.getFiles(clientDir, null);
		if (files.isEmpty()){
			log.error("No file to upload.");
			return false;
		}
		//
		ChannelSftp channelSftp = null;
		remoteDir = FileUtil.formatPath(remoteDir);
        try {
        	//
    		//connect to server
    		//
        	if (null == session || !session.isConnected()){
	    		if (!connect()) {//FATAL error
	    			return false;
	    		}
        	}
    		//
            //Open the SFTP channel
            //
            if (null == channel || !channel.isConnected()){
            	log.info("Opening Channel.");
		        channel = session.openChannel("sftp");
		        channel.connect();
		        channelSftp = (ChannelSftp)channel;
            }
            //
            //
            //
            String home = channelSftp.getHome();
            log.info("Home: " + home);
            //
            // PWD
            //
            @SuppressWarnings("rawtypes")
			Vector ls = channelSftp.ls(home);
            log.info("LIST directory: ");
            for (int i = 0; i < ls.size(); i++) {
            	com.jcraft.jsch.ChannelSftp.LsEntry lsEntry = (com.jcraft.jsch.ChannelSftp.LsEntry) ls.get(i);
                if (!lsEntry.getFilename().equals(".") && !lsEntry.getFilename().equals("..")) {
                	//
                	String fileName = lsEntry.getFilename();
                	log.info(fileName);
                }
			}
            
	        //
            //Change to the remote directory
            //
	        log.info("Changing to FTP remote dir: " + remoteDir);
	        channelSftp.cd(remoteDir);
	        //
            //Send the file we generated
            //
	        for (int i = 0; i < files.size(); i++) {
	        	String filename = (String)files.get(i);
	        	
	        	//upload
	        	if (!upFile(channelSftp, filename)){
	        		return false;
	        	}
			}
			//
			// Dispose ChannelSftp
			//
			channelSftp.quit();
			//
			// disconnect from server
			//
			if (!disConnect()){//FATAL error
				return false;
			}
			
        } catch (Exception e) {
        	log.error("Exception. " + e);
			return false;
		}
		return true;
	}
	
	/**
	 * upload a file to server
	 * @param channelSftp
	 * @param file
	 * @return boolean
	 */
	private boolean upFile(ChannelSftp channelSftp, String file) {
		
		boolean res = true;
		try {
			File f = new File(file);
			String fullName = f.getName();
			FileInputStream fis = new FileInputStream(f);
			channelSftp.put(fis, fullName);
			fis.close();
			fis = null;
			//
			log.info("Stored file as remote filename: " + fullName);
			retryCount = 0;

		} catch (Exception e) {
			
			if (retryCount < RETRY_MAX) {
				retryCount++;
				log.error("Stored remote file failed. " + e.toString());
				log.info("Stored Retry count=" + retryCount);
				try{
					Thread.sleep(retryInterval*1000);
				}catch (InterruptedException ee) {
					log.error("Thread sleep exception." + ee);
					return false;//FATAL error
				}
				res = upFile(channelSftp, file);
				
			}else{
				log.error("Cannot store file.");
				return false;
			}
		}
		return res;
	}

	/**
	 * Download list of files from SSH Server
	 * @param remoteDir
	 * @param destDir
	 * @return boolean
	 */
	public boolean download(String remoteDir, String destDir) {
		
		if (Utils.isNullEmpty(destDir)){
			log.error("Local directory is NULL");
			return false;
		}
		//
		ChannelSftp channelSftp = null;
		remoteDir = FileUtil.formatPath(remoteDir);
		destDir = FileUtil.formatPath(destDir);
		//
		try {
	        channel = session.openChannel("sftp");
	        channel.connect();
	        channelSftp = (ChannelSftp)channel;
	        //
            //Get the list of files in the remote server directory
            //
	        @SuppressWarnings("rawtypes")
			Vector files = channelSftp.ls(remoteDir);
            //
            //Log if we have nothing to download
            //
            if (files.size() == 0) {
            	log.warn("No files are available for download.");
            } else {
            	//
                //change to remote dir
                //
            	int cnt = 0;
                for (int i=0; i<files.size(); i++) {
                    com.jcraft.jsch.ChannelSftp.LsEntry lsEntry = (com.jcraft.jsch.ChannelSftp.LsEntry) files.get(i);
                    if (!lsEntry.getFilename().equals(".") && !lsEntry.getFilename().equals("..")) {
                    	//
                    	String fileName = lsEntry.getFilename();
                    		
                		log.info("Downloading file " + fileName);
                		//
                		channelSftp.cd(remoteDir);
                		//
                		String outputFileName = destDir + fileName;
                		//Get the write and write it to our local file system
                        File f = new File(outputFileName);     
                        FileOutputStream fos = new FileOutputStream(f);
                        channelSftp.get(fileName, fos);
                        fos.close();
                        fos = null;
            			//
                        cnt++;
                    }
                }
                if (cnt == 0){
                	log.warn("No files are available for download.");
                }
            }
            //
            //Disconnect from the FTP server
            //
            try {
            	channelSftp.quit();
            } catch (Exception exc) {
            	log.error("Unable to disconnect from FTP server. " + exc.toString());
            	return false;
            }
        } catch (Exception e) {
        	log.error("Exception. ");
			e.printStackTrace();
			return false;
		}
        return true;
	}
	
	/**
	 * send a command to server
	 * @param command 
	 * @return String
	 * @throws Exception 
	 */
	private String send(String command) throws Exception {
		
		String out = null;
        InputStream istream = channel.getInputStream();
        OutputStream ostream = channel.getOutputStream();
        Reader reader = new InputStreamReader(istream);
        Writer writer = new OutputStreamWriter(ostream);
        
    	log.info(">> " + command);
    	
    	try {
	        writer.write(command + "\n");
	        writer.flush();
	    } catch (IOException e) {
	    	log.error("Send command failed." + e);
	    	throw new Exception("Send command failed.", e);
	    } finally {
	    	writer.close();
	    	writer = null;
	    	ostream.close();
	    	ostream = null;
	    }
        
        try {
        	out = readResponse(reader, "$");
        } catch (Exception e) {
        	throw new Exception("Response failed. ", e);
	    } finally {
	    	reader.close();
	    	reader = null;
	    	istream.close();
	    	istream = null;
	    }
        return out;
	}

	/**
	 * read response from server
	 * @param reader
	 * @param targetMessage
	 * @return String
	 * @throws Exception
	 */
	private String readResponse(Reader reader, String targetMessage)
			throws Exception {
		
		StringBuffer buffer = new StringBuffer();
        boolean isReadCheck = false;
        
        try {
	        while (true) {
	            int c = reader.read();
	            if (c < 0) {
	            	break;
	            }
	        	buffer.append((char) c);
	        	if (buffer.toString().indexOf(targetMessage) > 0) {
	        	    isReadCheck = true;
	        	    break;
	        	}
	        }
        } catch(SocketTimeoutException e) {
        	log.error("readResponse SocketTimeoutException. " + e);
        	isReadCheck = false;
        }
        
        if (!isReadCheck) {
        	throw new Exception("readResponse >> " + buffer.toString());
        }
        String out = buffer.toString();
        
        if (Utils.isNullEmpty(out)) {
    		return out;
    	}
    	//
    	String[] lines = out.split("\n");
    	if (lines.length < 2) {
    		return out;
    	}
    	return out.substring(0, out.lastIndexOf("\n"));
	}
	
	/**
	 * Execute a command
	 * @param command
	 * @return String
	 * @throws Exception
	 */
	public String execCommand(String command) throws Exception {
		
		try {
			if (session.isConnected()){
				channel = session.openChannel("shell");
				channel.connect();
				return send(command);
			}
		} catch (Exception e) {
			throw e;
		}
		return null;
	}
	
	/**
	 * @return the retryInterval
	 */
	public int getRetryInterval() {
		return retryInterval;
	}

	/**
	 * @param retryInterval the retryInterval to set
	 */
	public void setRetryInterval(int retryInterval) {
		this.retryInterval = retryInterval;
	}
}
